Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/api/youtube-thumbnail/[id].ts
14425 views
1
/*
2
* This file is part of CoCalc: Copyright © 2026 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// Server-side proxy for YouTube video thumbnails. The click-to-load video
7
// gate (components/videos.tsx) uses this so visitors see a still image
8
// without their browser contacting i.ytimg.com / Google before they have
9
// explicitly consented to YouTube embeds.
10
//
11
// Thumbnails are effectively immutable per id, so we cache aggressively at
12
// the CDN / browser layer. We do not buffer in-process — Next.js / the
13
// upstream proxy handles concurrency fine, and adding a per-process LRU
14
// here would complicate cold start without measurable wins.
15
16
import type { NextApiRequest, NextApiResponse } from "next";
17
18
// YouTube video ids are 11 chars of [A-Za-z0-9_-]. Be slightly lenient
19
// (6-20) so a future id-length change doesn't silently break the page,
20
// while still rejecting obvious junk that would just produce a 404.
21
const ID_RE = /^[A-Za-z0-9_-]{6,20}$/;
22
23
// hqdefault is 480x360 with letterboxing for 16:9 sources — good enough
24
// for the carousel at 672px wide and always present. mqdefault is the
25
// fallback for the rare id where hqdefault is missing.
26
const VARIANTS = ["hqdefault", "mqdefault"] as const;
27
28
export default async function handler(
29
req: NextApiRequest,
30
res: NextApiResponse,
31
): Promise<void> {
32
const id = String(req.query.id ?? "");
33
if (!ID_RE.test(id)) {
34
res.status(400).send("invalid id");
35
return;
36
}
37
for (const variant of VARIANTS) {
38
const url = `https://i.ytimg.com/vi/${id}/${variant}.jpg`;
39
let upstream: Response;
40
try {
41
upstream = await fetch(url);
42
} catch (err) {
43
// Network blip; try next variant before giving up. We don't log
44
// every failure — a misconfigured firewall would otherwise flood
45
// the hub logs with one entry per page view.
46
continue;
47
}
48
if (!upstream.ok) continue;
49
const buf = Buffer.from(await upstream.arrayBuffer());
50
res.setHeader("content-type", "image/jpeg");
51
res.setHeader(
52
"cache-control",
53
"public, max-age=86400, s-maxage=604800, stale-while-revalidate=604800",
54
);
55
res.status(200).send(buf);
56
return;
57
}
58
res.status(404).send("not found");
59
}
60
61